#include <graphics/Mesh.h>
#include <graphics/MeshFileFormat.h>
#include <graphics/Engine.h>
#include <system/Log.h>

namespace ne
{
    Mesh::Mesh(ResourceManager *pManager,
               const uint64_t id,
               const std::string &name,
               const std::string &fileName,
               const bool isManual,
               ManualResourceLoader *pLoader)
        : Resource(pManager, id, name, fileName, isManual, pLoader)
    {
    }

    void Mesh::setVertexData(const VertexDataPtr &pVertexData)
    {
        mpVertexData = pVertexData;
    }

    VertexDataPtr Mesh::getVertexData() const
    {
        return mpVertexData;
    }

    bool Mesh::addChild(const SubMeshPtr &pChild)
    {
        if (pChild.notNull())
        {
            SubMeshMapConstIterator it = mChildren.find(pChild->getObjectName());
            if (it == mChildren.end())
            {
                mChildren[pChild->getObjectName()] = pChild;
                return true;
            }
        }
        return false;
    }

    void Mesh::removeChild(const SubMeshPtr &pChild)
    {
        if (pChild.notNull())
        {
            removeChild(pChild->getObjectName());
        }
    }

    void Mesh::removeChild(const std::string &name)
    {
        SubMeshMapIterator it = mChildren.find(name);
        if (it != mChildren.end())
        {
            mChildren.erase(it);
        }
    }

    void Mesh::removeAllChildren()
    {
        mChildren.clear();
    }

    SubMeshPtr Mesh::getChild(const std::string &name) const
    {
        SubMeshMapConstIterator it = mChildren.find(name);
        if (it != mChildren.end())
        {
            return it->second;
        }
        return SubMeshPtr();
    }

    size_t Mesh::getChildrenCount() const
    {
        return mChildren.size();
    }

    const SubMeshMap& Mesh::getChildrenMap() const
    {
        return mChildren;
    }

    void Mesh::setMaterial(const MaterialPtr &pMaterial)
    {
        mpMaterial = pMaterial;
    }

    MaterialPtr Mesh::getMaterial() const
    {
        return mpMaterial;
    }

    Mesh::~Mesh()
    {
        unload();
    }

    bool Mesh::loadImplement()
    {
        FILE *file = openFile("rb");
        if (!file)
        {
            Log::Out(LL_WARNING,
                "Open Mesh (%s) file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }

        MeshHeader meshHeader;
        if (!fread(&meshHeader, sizeof(MeshHeader), 1, file))
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }

        if (meshHeader.mMagicNum != MESH_FILE_MAGIC_NUMBER ||
            meshHeader.mVersion != MESH_FILE_VERSION)
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load Mesh (%s) from file (%s) with wrong Magic or Version",
                getName().c_str(), getFileName().c_str());
            return false;
        }

        // Create Material for Mesh
        std::string materialName = meshHeader.mMaterialName;
        if (!materialName.empty())
        {
            mpMaterial = MaterialManager::GetSingleton().createOrRetrieve(materialName, materialName).first;
            if (mpMaterial.notNull())
            {
                if (!mpMaterial->isLoaded())
                {
                    mpMaterial->load();
                }
            }
        }

        // Create Vertex Buffer
        if (meshHeader.mVertexBufferSize == 0 || meshHeader.mNumAttribute == 0)
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load Mesh (%s) from file (%s) with data",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        fseek(file, meshHeader.mVertexBufferOffset, SEEK_SET);
        GLBufferObjectPtr pVertexBuffer(GLBufferObjectManager::GetSingleton().createBufferObject(
            BOT_ARRAY_BUFFER, BOU_STATIC_DRAW, meshHeader.mVertexBufferSize));
        if (pVertexBuffer.isNull())
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Create VertexBuffer for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        uint32_t vertexBufferSource = pVertexBuffer->getSource();
        void* pVBuffer = pVertexBuffer->lock(BOA_WRITE_ONLY, 0, meshHeader.mVertexBufferSize, true);
        if (!fread(pVBuffer, meshHeader.mVertexBufferSize, 1, file))
        {
            pVertexBuffer->unlock();
            GLBufferObjectManager::GetSingleton().removeBySource(vertexBufferSource);
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load VertexBuffer for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        pVertexBuffer->unlock();

        // Create Vertex Data
        mpVertexData.bind(new VertexData(PrimitiveType(meshHeader.mPrimitiveType)));
        if (mpVertexData.isNull())
        {
            GLBufferObjectManager::GetSingleton().removeBySource(vertexBufferSource);
            closeFile(file);
            Log::Out(LL_WARNING,
                "Create VertexData for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        mpVertexData->setStride(meshHeader.mStride);

        // Create Vertex Attribute
        fseek(file, meshHeader.mAttributeOffset, SEEK_SET);
        VertexAttributeInfo attributeInfos[meshHeader.mNumAttribute];
        if (!fread(attributeInfos, sizeof(VertexAttributeInfo) * meshHeader.mNumAttribute, 1, file))
        {
            GLBufferObjectManager::GetSingleton().removeBySource(vertexBufferSource);
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load VertexAttribute for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        for (uint32_t i=0; i<meshHeader.mNumAttribute; ++i)
        {
            mpVertexData->createAttribute(vertexBufferSource,
                AttributeSemantic(attributeInfos[i].mSemantic),
                AttributeType(attributeInfos[i].mType),
                attributeInfos[i].mCount,
                attributeInfos[i].mStride,
                attributeInfos[i].mOffset,
                bool(attributeInfos[i].mNormalized));
        }

        // Create Index Buffer
        if (meshHeader.mIndexBufferSize == 0 || meshHeader.mNumSubMesh == 0)
        {
            closeFile(file);
            return true;
        }
        fseek(file, meshHeader.mIndexBufferOffset, SEEK_SET);
        GLBufferObjectPtr pIndexBuffer(GLBufferObjectManager::GetSingleton().createBufferObject(
            BOT_ELEMENT_ARRAY_BUFFER, BOU_STATIC_DRAW, meshHeader.mIndexBufferSize));
        if (pIndexBuffer.isNull())
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Create IndexBuffer for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        uint32_t indexBufferSource = pIndexBuffer->getSource();
        void* pIBuffer = pIndexBuffer->lock(BOA_WRITE_ONLY, 0, meshHeader.mIndexBufferSize, true);
        if (!fread(pIBuffer, meshHeader.mIndexBufferSize, 1, file))
        {
            pIndexBuffer->unlock();
            GLBufferObjectManager::GetSingleton().removeBySource(indexBufferSource);
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load IndexBuffer for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        pIndexBuffer->unlock();

        // Create SubMesh
        fseek(file, meshHeader.mSubMeshOffset, SEEK_SET);
        SubMeshHeader subMeshHeaders[meshHeader.mNumSubMesh];
        if (!fread(subMeshHeaders, sizeof(SubMeshHeader) * meshHeader.mNumSubMesh, 1, file))
        {
            GLBufferObjectManager::GetSingleton().removeBySource(indexBufferSource);
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load SubMesh for Mesh (%s) from file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }
        for (uint32_t i=0; i<meshHeader.mNumSubMesh; ++i)
        {
            SubMeshPtr pSubMesh(new SubMesh(subMeshHeaders[i].mName));
            // Create Material for SubMesh
            std::string materialName = subMeshHeaders[i].mMaterialName;
            if (!materialName.empty())
            {
                MaterialPtr pMaterial(MaterialManager::GetSingleton().createOrRetrieve(materialName, materialName).first);
                if (pMaterial.notNull())
                {
                    if (!pMaterial->isLoaded())
                    {
                        pMaterial->load();
                    }
                    pSubMesh->setMaterial(pMaterial);
                }
            }

            // Create IndexData for SubMesh
            IndexDataPtr pIndexData(new IndexData(
                PrimitiveType(subMeshHeaders[i].mPrimitiveType),
                indexBufferSource,
                AttributeType(subMeshHeaders[i].mType),
                subMeshHeaders[i].mCount,
                subMeshHeaders[i].mOffset));
            if (pIndexData.notNull())
            {
                pSubMesh->setIndexData(pIndexData);
            }
            addChild(pSubMesh);
        }

        closeFile(file);

        return true;
    }

    bool Mesh::unloadImplement()
    {
        mpMaterial.setNull();
        removeAllChildren();
        return true;
    }

    bool Mesh::downloadImplement()
    {
        FILE *file = openFile("wb");
        if (!file)
        {
            Log::Out(LL_WARNING,
                "Create Mesh (%s) file (%s) failed",
                getName().c_str(), getFileName().c_str());
            return false;
        }

        MeshHeader meshHeader;
        memset(&meshHeader, 0, sizeof(MeshHeader));
        fwrite(&meshHeader, sizeof(MeshHeader), 1, file);

        meshHeader.mMagicNum = MESH_FILE_MAGIC_NUMBER;
        meshHeader.mVersion = MESH_FILE_VERSION;
        strncpy(meshHeader.mName, mName.c_str(),
                MAX_STRING_LENGTH_IN_MESH_FILE);

        // Save Material name
        if (mpMaterial.notNull())
        {
            strncpy(meshHeader.mMaterialName,
                    mpMaterial->getName().c_str(),
                    MAX_STRING_LENGTH_IN_MESH_FILE);
        }

        if (mpVertexData.notNull())
        {
            meshHeader.mPrimitiveType = mpVertexData->getPrimitiveType();
            meshHeader.mStride = 0;
            meshHeader.mNumAttribute = mpVertexData->getAttributeCount();
            meshHeader.mAttributeOffset = ftell(file);

            VertexAttributeInfo attributeInfos[meshHeader.mNumAttribute];
            memset(attributeInfos, 0, sizeof(VertexAttributeInfo) * meshHeader.mNumAttribute);
            const VertexAttributeMap &attributeMap = mpVertexData->getAttributeMap();
            size_t i = 0;
            uint32_t offset = 0;
            for (VertexAttributeMapConstIterator it=attributeMap.begin(); it!=attributeMap.end(); ++it)
            {
                VertexAttributePtr pAttribute = it->second;
                if (pAttribute.notNull())
                {
                    attributeInfos[i].mSemantic = pAttribute->getSemantic();
                    attributeInfos[i].mType = pAttribute->getType();
                    attributeInfos[i].mCount = pAttribute->getCount();
                    attributeInfos[i].mOffset = offset;
                    attributeInfos[i].mNormalized = pAttribute->isNormalized();

                    offset += pAttribute->getAttributeSize();
                    meshHeader.mStride = offset;
                    ++i;
                }
            }

            // TODO: Here need to create the vertex buffer
            meshHeader.mVertexBufferSize = mpVertexData->getSize();
            byte *vertexBuffer = new byte[meshHeader.mVertexBufferSize];
            memset(vertexBuffer, 0, meshHeader.mVertexBufferSize);
            i = 0;
            for (VertexAttributeMapConstIterator it=attributeMap.begin(); it!=attributeMap.end(); ++it)
            {
                VertexAttributePtr pAttribute = it->second;
                if (pAttribute.notNull())
                {
                    attributeInfos[i].mStride = meshHeader.mStride;
                    GLBufferObjectPtr pVertexBuffer = GLBufferObjectManager::GetSingleton().getBufferObject(pAttribute->getSource());
                    if (pVertexBuffer.notNull())
                    {
                        byte* pVBuffer = static_cast<byte*>(pVertexBuffer->lock(BOA_READ_ONLY, 0, pVertexBuffer->getSize(), false));
                        if (pVBuffer)
                        {
                            byte* pBuffer = &vertexBuffer[attributeInfos[i].mOffset];
                            pVBuffer += pAttribute->getOffset();
                            uint32_t stride = pAttribute->getStride();
                            if (stride == 0) stride = pAttribute->getAttributeSize();

                            for (uint32_t j=0; j<attributeInfos[i].mCount; ++j)
                            {
                                memcpy(pBuffer, pVBuffer, pAttribute->getAttributeSize());
                                pBuffer += meshHeader.mStride;
                                pVBuffer += stride;
                            }
                        }
                        pVertexBuffer->unlock();
                    }
                    ++i;
                }
            }

            fwrite(attributeInfos, sizeof(VertexAttributeInfo) * meshHeader.mNumAttribute, 1, file);
            meshHeader.mVertexBufferOffset = ftell(file);
            fwrite(vertexBuffer, meshHeader.mVertexBufferSize, 1, file);
            delete[] vertexBuffer;
        }
        
        if (getChildrenCount())
        {
            meshHeader.mNumSubMesh = uint32_t(getChildrenCount());
            meshHeader.mSubMeshOffset = ftell(file);

            meshHeader.mIndexBufferSize = 0;
            SubMeshHeader subMeshHeaders[meshHeader.mNumSubMesh];
            memset(subMeshHeaders, 0, sizeof(SubMeshHeader) * meshHeader.mNumSubMesh);
            size_t i = 0;
            for (SubMeshMapConstIterator it=mChildren.begin(); it!=mChildren.end(); ++it)
            {
                SubMeshPtr pSubMesh = it->second;
                if (pSubMesh.notNull())
                {
                    strncpy(subMeshHeaders[i].mName,
                            pSubMesh->getObjectName().c_str(),
                            MAX_STRING_LENGTH_IN_MESH_FILE);

                    MaterialPtr pMaterial = pSubMesh->getMaterial();
                    if (pMaterial.notNull())
                    {
                        strncpy(subMeshHeaders[i].mMaterialName,
                                pMaterial->getName().c_str(),
                                MAX_STRING_LENGTH_IN_MESH_FILE);
                    }

                    IndexDataPtr pIndexData = pSubMesh->getIndexData();
                    if (pIndexData.notNull())
                    {
                        subMeshHeaders[i].mPrimitiveType = pIndexData->getPrimitiveType();
                        subMeshHeaders[i].mType = pIndexData->getType();
                        subMeshHeaders[i].mCount = pIndexData->getCount();
                        subMeshHeaders[i].mOffset = meshHeader.mIndexBufferSize;

                        meshHeader.mIndexBufferSize += pIndexData->getSize();
                        ++i;
                    }
                }
            }
            fwrite(subMeshHeaders, sizeof(SubMeshHeader) * meshHeader.mNumSubMesh, 1, file);

            meshHeader.mIndexBufferOffset = ftell(file);
            // TODO: Here need to create the index buffer
            byte *indexBuffer = new byte[meshHeader.mIndexBufferSize];
            memset(indexBuffer, 0, meshHeader.mIndexBufferSize);
            i = 0;
            for (SubMeshMapConstIterator it=mChildren.begin(); it!=mChildren.end(); ++it)
            {
                SubMeshPtr pSubMesh = it->second;
                if (pSubMesh.notNull())
                {
                    IndexDataPtr pIndexData = pSubMesh->getIndexData();
                    if (pIndexData.notNull())
                    {
                        byte* pBuffer = &indexBuffer[subMeshHeaders[i].mOffset];
                        GLBufferObjectPtr pIndexBuffer = GLBufferObjectManager::GetSingleton().getBufferObject(pIndexData->getSource());
                        if (pIndexBuffer.notNull())
                        {
                            byte* pIBuffer = static_cast<byte*>(pIndexBuffer->lock(BOA_READ_ONLY, 0, pIndexBuffer->getSize(), false));
                            if (pIBuffer)
                            {
                                pIBuffer += pIndexData->getOffset();
                                memcpy(pBuffer, pIBuffer, pIndexData->getSize());
                            }
                            pIndexBuffer->unlock();
                        }
                        ++i;
                    }
                }
            }
            fwrite(indexBuffer, meshHeader.mIndexBufferSize, 1, file);
            delete[] indexBuffer;
        }

        fseek(file, 0, SEEK_SET);
        fwrite(&meshHeader, sizeof(MeshHeader), 1, file);
        closeFile(file);

        return true;
    }

    size_t Mesh::_calculateSize() const
    {
        size_t size = 0;
        for (SubMeshMapConstIterator it=mChildren.begin(); it!=mChildren.end(); ++it)
        {
            size += it->second->getSize();
        }
        return size;
    }
}
